package asynctask

import (
	"encoding/json"
	"fmt"
	"scase.io/application-auto-scaling-service/pkg/utils"
	"strings"
	"time"

	ecsmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/ecs/v2/model"
	"github.com/pkg/errors"
	"scase.io/application-auto-scaling-service/pkg/api/model"
	"scase.io/application-auto-scaling-service/pkg/cloudresource"
	"scase.io/application-auto-scaling-service/pkg/common"
	"scase.io/application-auto-scaling-service/pkg/db"
	event "scase.io/application-auto-scaling-service/pkg/service/event"
	"scase.io/application-auto-scaling-service/pkg/utils/logger"
)

const (
	TaskTypeEcsScaleOut = db.TaskTypeScaleOutEcsGroup
	MaxWaitTimes        = 40
	defaultRandomStrLen = 8
)

var (
	ErrEcsCreateFail = errors.New("ecs create failed")
)

// ScaleOutTask 扩容任务
type EcsScaleOutTask struct {
	TaskId      string
	GroupId     string
	InstanceNum int32
	BaseTask
	event *event.EventService
}

// NewScaleOutTask ...
func NewEcsScaleOutTask(taskId string, groupId string) *EcsScaleOutTask {
	return &EcsScaleOutTask{
		TaskId:  taskId,
		GroupId: groupId,
	}
}

// GetKey get id of the resource corresponding to the task
func (t *EcsScaleOutTask) GetKey() string {
	return t.GroupId
}

// GetType get task type
func (t *EcsScaleOutTask) GetType() string {
	return TaskTypeEcsScaleOut
}

func (t *EcsScaleOutTask) Run(log *logger.FMLogger) error {
	if err := t.initTask(log); err != nil {
		log.Error("[e_scale_out] init task failed with error:%+v", err)
		return db.TxRecordEcsGroupScalingTaskComplete(t.GroupId, t.TaskId)
	}
	group, err := db.ScalingGroupTable().GetScalingGroupById(t.GroupId)
	if err != nil {
		log.Error("[e_scale_out] get scaling group [%s] error:%+v", t.GroupId)
		return err
	}
	resCtl, err := cloudresource.GetEcsController(group.ProjectId)
	if err != nil {
		log.Error("[e_scale_out] GetEcsController error:%+v", err)
		return err
	}
	t.event, err = event.NewEventService(group.ProjectId, nil, log)
	if err != nil {
		log.Error("[e_scale_out] new event service error:%+v", err)
		return err
	}
	// 1. create ecs
	jobId, err := t.handlerCreate(log, group, resCtl)
	if err != nil {
		log.Error("[e_scale_out] create ecs task error:%+v", err)
		eveReq := t.event.BuildInstanceEventReq(fmt.Sprintf("[Scale-out-%d-vm] failed", t.InstanceNum), group.Id, fmt.Sprintf("create vm task [%s] failed with error:%+v", t.TaskId, err.Error()),
			fmt.Sprintf("fleet [%s] scale out %d instance failed", group.FleetId, t.InstanceNum), common.EventTraceStateIncident)
		t.event.RegisterEvent(eveReq, group.ProjectId, log)
		return err
	}

	// 2. wait create
	ecsIdList, err := t.WaitEcsCreate(jobId, resCtl, group, log)
	if err != nil {
		log.Error("[e_scale_out] create ecs failed, job[%s], error:%+v", jobId, err)
		return err
	}
	eveReq := t.event.BuildInstanceEventReq(fmt.Sprintf("[Scale-out-%d-vm] success", t.InstanceNum), group.Id, fmt.Sprintf("create %d vm success, job id: %s, vmId: %s", t.InstanceNum, jobId, ecsIdList),
		fmt.Sprintf("fleet [%s] scale out %d instance success", group.FleetId, t.InstanceNum), common.EventTraceStateNormal)
	t.event.RegisterEvent(eveReq, group.ProjectId, log)
	log.Info("[e_scale_out] create ecs for group [%s] success, taskId [%s]", t.GroupId, t.TaskId)
	return db.TxRecordEcsGroupScalingTaskComplete(t.GroupId, t.TaskId)
}

func (t *EcsScaleOutTask) initTask(log *logger.FMLogger) error {
	// taskConf中存的是初始创建时的数量，affectNum是剔除成功的部分还要再创建的数量，任务失败时会修改affectNum，t.Instance当该任务的全局变量用
	taskInfo, err := db.GetAsyncTaskByTaskId(t.TaskId)
	if err != nil {
		log.Error("[e_scale_out] get task [%s] error:%+v", err)
		return err
	}
	if taskInfo.AffectNum <= 0 {
		log.Error("[e_scale_out] task [%s] will create 0 instance, quit task", t.TaskId)
		return fmt.Errorf("[e_scale_out] task [%s] will create 0 instance, quit task", t.TaskId)
	}
	t.InstanceNum = taskInfo.AffectNum
	taskRetryTimes := t.GetRetryTimes()
	if taskRetryTimes > common.AsyncScaleOutMaxRetryTimes {
		log.Error("[e_scale_out] task [%s] execute fail after retry %d times, give up on this task", t.TaskId, taskRetryTimes)
		// 任务下发失败时修改期望实例数
		UpdateDesireNum(log, t.GroupId, t.InstanceNum)
		return fmt.Errorf("[e_scale_out] task [%s] execute fail after retry %d times, give up on this task", t.TaskId, taskRetryTimes)
	}
	log.Info("[e_scale_out] start create ecs, taskId:[%s], groupId:[%s], number of instance:%d", t.TaskId, t.GroupId, t.InstanceNum)
	return nil
}

func (t *EcsScaleOutTask) handlerCreate(log *logger.FMLogger, group *db.ScalingGroup, resCtl *cloudresource.EcsResourceController) (string, error) {
	// 如果发现taskId已经关联了一个jobid，说明任务执行到一半程序中断了，此时去执行等待任务即可
	if job := db.GetScaleOutTaskJobId(t.TaskId); job != "" {
		log.Info("task [%s] has job [%s], no need to create and doing waiting create method", t.TaskId, job)
		return job, nil
	}
	// 遍历规格创建，如果所有规格都无法创建，记录本次失败的活动记录
	vmTemplate := model.VmTemplate{}
	if err := json.Unmarshal([]byte(group.VmTemplate), &vmTemplate); err != nil {
		log.Error("[e_scale_out] parse vmTemplate err:%+v", err)
		return "", err
	}
	var jobId string
	availableFlavor := vmTemplate.AvailableFlavorIds
	AzWithFlavorList := t.BuildAzWithFlavor(&vmTemplate)
	errList := []string{}
	eveReq := t.event.BuildInstanceEventReq(fmt.Sprintf("[Scale-out-%d-vm] start", t.InstanceNum), group.Id, fmt.Sprintf("create %d vm job start, flavor:%s", t.InstanceNum, availableFlavor),
		fmt.Sprintf("fleet [%s] scale out %d instance start", group.FleetId, t.InstanceNum), common.EventTraceStateNormal)
	t.event.RegisterEvent(eveReq, group.ProjectId, log)
	for _, awf := range AzWithFlavorList {
		createEcsReq := t.convertModel(group, &vmTemplate, awf[0], awf[1])
		createResp, err := resCtl.CreatePrePaidEcs(log, createEcsReq)
		if err != nil {
			log.Error("[e_scale_out] create ecs failed with error:%+v", err)
			errList = append(errList, err.Error())
			continue
		}
		jobId = *createResp.JobId
		break
	}

	if len(jobId) == 0 {
		log.Error("[e_scale_out] create ecs failed after try all flavors:%s", availableFlavor)
		return "", errors.Wrapf(ErrEcsCreateFail, "create ecs failed after try all flavors:%s, error:%s", availableFlavor, strings.Join(errList, ","))
	}
	if err := db.AddJobIdToScaleOutTask(jobId, t.TaskId); err != nil {
		log.Error("[e_scale_out] insert job id [%s] to task [%s] error:%+v", jobId, t.TaskId, err)
	}
	return jobId, nil
}

func (t *EcsScaleOutTask) convertModel(group *db.ScalingGroup, vmTemplate *model.VmTemplate, az, flavor string) *model.CreateEcsReqModel {
	createEcsModel := model.CreateEcsReqModel{
		FleetId:             group.FleetId,
		ProjectId:           group.ProjectId,
		SubnetId:            group.SubnetId,
		VpcId:               group.VpcId,
		KeyName:             *vmTemplate.KeyName,
		ImageID:             *vmTemplate.ImageID,
		Disks:               vmTemplate.Disks,
		SecurityGroups:      vmTemplate.SecurityGroups,
		Eip:                 vmTemplate.Eip,
		VmNum:               int(t.InstanceNum),
		FlavorId:            flavor,
		AvailableZone:       az,
		EcsGroupId:          group.Id,
		EnterpriseProjectId: group.EnterpriseProjectId,
		AgencyName:          group.AgencyName,
	}
	tags := &[]model.InstanceTag{}
	if err := utils.ToObject([]byte(group.InstanceTags), tags); err != nil {
		createEcsModel.InstanceTags = nil
	} else {
		createEcsModel.InstanceTags = *tags
	}
	name := fmt.Sprintf("Fleet_%s_group_%s", group.FleetId, common.RandStr(defaultRandomStrLen))
	createEcsModel.Name = name
	return &createEcsModel
}

// 构造出{az,flavor}的序列，通过遍历序列实现在某az上创建某规格的ecs
// 如果az为空，创建ecs会随机指定az，字段构建成{"",flavorId}
func (t *EcsScaleOutTask) BuildAzWithFlavor(vmTemplate *model.VmTemplate) [][]string {
	var awf [][]string
	if len(vmTemplate.AvailableZones) == 0 {
		vmTemplate.AvailableZones = append(vmTemplate.AvailableZones, "")
	}
	for _, az := range vmTemplate.AvailableZones {
		for _, af := range vmTemplate.AvailableFlavorIds {
			awf = append(awf, []string{az, af})
		}
	}
	return awf
}

func (t *EcsScaleOutTask) WaitEcsCreate(jobId string, resCtl *cloudresource.EcsResourceController, group *db.ScalingGroup, log *logger.FMLogger) ([]string, error) {
	finishFlag := false
	for i := 0; i < MaxWaitTimes; i++ {
		showJobResp, err := resCtl.ShowJob(jobId)
		if err != nil {
			log.Error("[e_scale_out] show job error:%+v", err)
			return nil, err
		}
		if *showJobResp.Status == ecsmodel.GetShowJobResponseStatusEnum().SUCCESS {
			log.Info("[e_scale_out] job id [%s] finish all sub jobs", jobId)
			finishFlag = true
			break
		}
		if *showJobResp.Status == ecsmodel.GetShowJobResponseStatusEnum().FAIL {
			log.Error("[e_scale_out] job id [%s] finish but fail", jobId)
			t.handleFailJob(jobId, showJobResp, group, log)
			break
		}
		time.Sleep(common.SleepSecond * time.Second)
	}
	ecsIdList := t.insertSuccessfullyCreatedServers(log, group, resCtl, jobId)
	if !finishFlag {
		if err := db.AddJobIdToScaleOutTask("", t.TaskId); err != nil {
			log.Error("[e_scale_out] reset job id to task[%s],error:%+v", err)
		}
		log.Error("[e_scale_out] job [%s] is fail or not finish after retry %s times", jobId, MaxWaitTimes)
		return nil, fmt.Errorf("job [%s] is not finish", jobId)
	}
	return ecsIdList, nil
}

func (t *EcsScaleOutTask) handleFailJob(jobId string, showJobResp *ecsmodel.ShowJobResponse,
	group *db.ScalingGroup, log *logger.FMLogger) {
	failSubJobCount := int32(getFailSubJobCount(showJobResp))
	if failSubJobCount == 0 {
		log.Warn("there are no fail sub job of job [%s]", jobId)
		return
	}
	if err := db.UpdateAsyncTaskAffectNum(t.TaskId, failSubJobCount); err != nil {
		log.Error("[e_scale_out] update affect num error:%+v", err)
		return
	}

	var failInfo string
	// 防止空指针引用
	if showJobResp.FailReason == nil {
		failInfo = "fail reason is empty"
	} else {
		failInfo = *showJobResp.FailReason
	}

	failReason := fmt.Sprintf("create instance fail, create num: %d, fail: %d, job fail reasion: %s", t.InstanceNum, failSubJobCount, failInfo)
	eveReq := t.event.BuildInstanceEventReq("[create-vm] failed", group.Id, failReason,
		fmt.Sprintf("group [%s] scale out %d instance failed", t.GroupId, t.InstanceNum), common.EventTraceStateIncident)
	t.event.RegisterEvent(eveReq, group.ProjectId, log)
}

func getFailSubJobCount(showJobResp *ecsmodel.ShowJobResponse) int {
	count := 0
	for _, subJob := range *showJobResp.Entities.SubJobs {
		if *subJob.Status == ecsmodel.GetSubJobStatusEnum().FAIL {
			count++
		}
	}
	return count
}

func (t *EcsScaleOutTask) insertSuccessfullyCreatedServers(log *logger.FMLogger, group *db.ScalingGroup,
	resCtl *cloudresource.EcsResourceController, jobId string) []string {
	showJobResp, err := resCtl.ShowJob(jobId)
	if err != nil {
		log.Error("[e_scale_out] show job error:%+v", err)
		return nil
	}
	var ecsIdList []string
	for _, subjob := range *showJobResp.Entities.SubJobs {
		if *subjob.Status == ecsmodel.GetSubJobStatusEnum().SUCCESS {
			ecsinfo := db.EcsInfo{
				InstanceId: *subjob.Entities.ServerId,
				GroupId:    t.GroupId,
				JobId:      jobId,
				FleetId:    group.FleetId,
				TaskId:     t.TaskId,
				ProjectId:  group.ProjectId,
				State:      db.EcsStateActivating,
				TimeModel: db.TimeModel{
					IsDeleted: "0",
				},
			}
			ecsIdList = append(ecsIdList, ecsinfo.InstanceId)
			showServer, err := resCtl.ShowServer(*subjob.Entities.ServerId)
			if err != nil {
				ecsinfo.Name = fmt.Sprintf("Fleet_%s_group", group.FleetId)
			} else {
				ecsinfo.Name = showServer.Server.Name
				ecsinfo.CreateAt, err = time.Parse(common.TimeLayout, showServer.Server.Created)
				if err != nil {
					ecsinfo.CreateAt = time.Now().UTC()
				}
			}
			if err := db.EcsInfoTable().InsertOrUpdate(&ecsinfo); err != nil {
				log.Error("[e_scale_out] insert new activating instance [%s] error:%+v", *subjob.Entities.ServerId, err)
				continue
			}
		}
	}
	// 实例数量也要及时更新，否则伸缩活动基准有问题
	if err := db.UpdateCurrentNumber(group.Id, log); err != nil {
		log.Error("[e_scale_out] update group [%s] current instance num error:%+v", group.Id, err)
	}
	return ecsIdList
}
